﻿// UTILITIES

jQuery.mapEx = function(input, mapping) {
    if (input && input.jquery)
        return input.map(mapping);
    return mapping.call(input);
};
jQuery.getEx = function(input) {
    if (input && input.jquery) {
        if (input.length === 1) {
            return input[0];
        } else if (input.length > 1) {
            throw "Not supported on multiple input.";
        }
    }
    return input;
};
jQuery.ret = function(ret) {
    return function() {
        ret(this);
    };
};
jQuery.nop = function() {};
jQuery.pass = function(value) {
    return value;
};

jQuery.builder = function(target) {
    var counter = 0;
    var consume;
    var builder = function(ret) {
        if (counter === 0) {
            ret(target);
        } else
            consume = ret;
    };
    builder.consumer = function(action) {
        return function(key) {
            counter++;
            return function(value) {
                action(target, value, key);
                if (--counter === 0 && consume)
                    consume(target);
            };
        };
    };
    return builder;
};

jQuery.listBuilder = function() {
    var builder = jQuery.builder([]);
    builder.adder = builder.consumer(function(target, value) {
        target.push(value);
    });
    builder.adderMany = builder.consumer(function(target, value) {
        if (value instanceof Array) {
            target.push.apply(target, value);
        } else {
            target.push(value);
        }
    });
    return builder;
};
jQuery.mapBuilder = function() {
    var builder = jQuery.builder({});
    builder.setter = builder.consumer(function(target, value, key) {
        if (typeof(key) === "function") {
            key(target, value);
        } else {
            target[key] = value;
        }
    });
    return builder;
};
jQuery.mergeObjects = function(/*objs...*/) {
    var result = {};
    for (var i = 0; i < arguments.length; i++) {
        var obj = arguments[i];
        for (var key in obj) {
            result[key] = obj[key];
        }
    }
    return result;
};
jQuery.mergeMapList = function(target, source) {
    for (var k in source) {
        var sourceList = source[k];
        var targetList = target[k];
        if (targetList) {
            targetList.push.apply(targetList, sourceList);
        } else {
            target[k] = sourceList;
        }
    }
};
jQuery.deriveObject = function(prototype) {
    var derived = function() {};
    derived.prototype = prototype;
    return new derived();
};

const DOCUMENT_POSITION_DISCONNECTED = 0x01;
const DOCUMENT_POSITION_PRECEDING = 0x02;
const DOCUMENT_POSITION_FOLLOWING = 0x04;
const DOCUMENT_POSITION_CONTAINS = 0x08;
const DOCUMENT_POSITION_CONTAINED_BY = 0x10;

jQuery.rangeFilter = function(start, stop) {
    return function() {
        return this.compareDocumentPosition(start) == DOCUMENT_POSITION_PRECEDING
            && (!stop || this.compareDocumentPosition(stop) == DOCUMENT_POSITION_FOLLOWING);
    };
};

// EXTRACTOR

function extract(def, log, ret) {
    extract.url(function(window, openUrl) {
        extract.process({
            openUrl: openUrl,
            log: log,
            defs: {},
            values: {}
        }, def, null, ret, null, null);
    }, def.delay, log);
}
extract.url = function(process, delay, log) {
    log("initializing (delay = " + delay + ") ... ");
    var waitList = [];
    var openUrl = function(url, process) {
        waitList.push({url: url, process: process});
    };
    var current = {process: function(window) {
        process(window, openUrl);
    }};

    var mainWindow = window.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
                       .getInterface(Components.interfaces.nsIWebNavigation)
                       .QueryInterface(Components.interfaces.nsIDocShellTreeItem)
                       .rootTreeItem
                       .QueryInterface(Components.interfaces.nsIInterfaceRequestor)
                       .getInterface(Components.interfaces.nsIDOMWindow);

    var browser = mainWindow.gBrowser;

    var tab = browser.addTab("about:blank");
    //var tab = jetpack.tabs.open("about:blank");

    var waiting = false;

    tab.addEventListener("load", function (event) {
        var window = event.target.linkedBrowser.contentWindow;
        if (waiting)
            return; // jetpack bug? ignore!
        log("processing ... ");
        current.process(window);
        log("done\r\n");
        current = waitList.shift();
        if (current) {
            waiting = true;
            log(current.url + " ... waiting ... ");
            window.setTimeout(function() {
                waiting = false;
                log("loading ... ");
                window.location.href = current.url;
            }, delay);
        } else {
            log("finished");
            tab.close();
        }
    }, true);
    /*
    tab.onReady(function() {
        var window = this.contentWindow.wrappedJSObject;
        if (waiting)
            return; // jetpack bug? ignore!
        log("processing ... ");
        current.process(window);
        log("done\r\n");
        current = waitList.shift();
        if (current) {
            waiting = true;
            log(current.url + " ... waiting ... ");
            window.setTimeout(function() {
                waiting = false;
                log("loading ... ");
                window.location.href = current.url;
            }, delay);
        } else {
            log("finished");
            tab.close();
        }
    });*/
};

extract.process = function($, def, value, ret, thisHeader, nextHeader) {
    if (def.def)
        $.defs = jQuery.mergeObjects($.defs, def.def);
    if (def.use) {
        // beware def.use is not our Array, but the pages array, you cannot use it in apply
        var useDefs = Array.prototype.map.call(def.use, function(use) {
            var useDef = $.defs[use];
            if (!useDef)
                throw "Undefined named use: " + use;
            return useDef;
        });
        useDefs.push(def);
        def = jQuery.mergeObjects.apply(jQuery.mergeObjects, useDefs);
    }
    if (def.name) {
        $.defs[def.name] = def;
    }
    if (def.url) {
        if (def.next) {
            var builder = jQuery.listBuilder();
            extract.process.url($, def.url, def, value, builder.adderMany(), builder.adderMany, thisHeader, nextHeader);
            builder(ret);
        } else {
            extract.process.url($, def.url, def, value, ret, null, thisHeader, nextHeader);
        }
    } else {
        extract.process.core($, def, value, ret, thisHeader, nextHeader);
    }
};
extract.process.url = function($, url, def, value, ret, adder, thisHeader, nextHeader) {
    if (typeof(url) === "string") {
        extract.process.load($, url, def, ret, adder);
    } else {
        extract.process($, url, value, function(finalUrl) {
            extract.process.load($, finalUrl, def, ret, adder);
        }, thisHeader, nextHeader);
    }
};
extract.process.load = function($, url, def, ret, adder) {
    if (!url) {
        ret([]);
        return;
    }
    if (typeof(url) !== "string")
        throw "Url is not string: " + url;
    $.openUrl(url, function(window) {
        var jq = jQuery.install(window);
        jq.openUrl = $.openUrl;
        jq.log = $.log;
        jq.defs = jQuery.deriveObject($.defs);
        jq.values = jQuery.deriveObject($.values);
        var nextRet = adder ? adder() : null;
        extract.process.core(jq, def, window.document, ret, null, null);
        if (nextRet)
            extract.process.url(jq, def.next, def, window.document, nextRet, adder);
    });
};
extract.process.core = function($, def, value, ret, thisHeader, nextHeader) {
    if (def.get) {
        value = $.values[def.get];
    }
    if (def.area) {
        value = $(value).find(def.area);
        if (thisHeader) {
            value = value.filter(jQuery.rangeFilter(thisHeader, nextHeader));
            thisHeader = null;
        }
    }
    if (def.header) {
        var value = $(value);
        var headers = value.find(def.header);
        if (thisHeader) {
            // FIXME does not work if :eq or ... used
            headers = headers.filter(jQuery.rangeFilter(thisHeader, nextHeader));
            headers[headers.length] = nextHeader;
        }
        var builder = jQuery.listBuilder();
        for (var i = 0; i < headers.length; i++) {
            extract.process.item($, def, value, builder.adderMany(), headers[i], headers[i + 1]);
        }
        builder(ret);
    } else {
        if (thisHeader)
            value = thisHeader;
        extract.process.item($, def, value, ret, null, null);
    }
};
extract.process.item = function($, def, value, ret, thisHeader, nextHeader) {
    if (def.read)
        value = extract.read(def.read).call(jQuery.getEx(value));
    if (def.regexp)
        value = extract.regexp(def.regexp).call(jQuery.getEx(value));
    if (def.let) {
        var lets = def.let;
        for (var k in lets) {
            var l = lets[k];
            if (typeof(l) === "object") {
                extract.process($, l, value, function(v) {
                    $.values[k] = v;
                }, thisHeader, nextHeader);
            } else {
                $.values[k] = l;
            }
        }
    }
    if (def.select) {
        extract.adder(def.adder)(value, extract.select($, def.select, thisHeader, nextHeader), ret);
    } else {
        ret(value);
    }
};

// SELECT implementation
extract.select = function($, def, thisHeader, nextHeader) {
    return function(ret) {
        var builder = jQuery.mapBuilder();
        for (var key in def) {
            extract.process($, def[key], this, builder.setter(extract.attach(def[key].attach)(key)), thisHeader, nextHeader);
        }
        builder(ret);
    };
};

// READ implementation
extract.read = function(def) {
    return extract.read[def];
};
extract.read.link = function() {
    return this.href;
};
extract.read.text = function() {
    return this.textContent;
};
extract.read.html = function() {
    return this.innerHTML;
};

// ATTACH implementation
extract.attach = function(def) {
    return extract.attach[def] || jQuery.pass;
};
extract.attach.inline = function(key) {
    return function(target, value) {
        if (value.length === 1) {
            var source = value[0];
            for (var k in source) {
                target[k] = source[k];
            }
        } else if (value.length > 1) {
            throw "Cannot attach inline multiple match.";
        }
    };
};
extract.attach.single = function(key) {
    return function(target, value) {
        if (value.length === 1) {
            var source = value[0];
            target[key] = source;
        } else if (value.length > 1) {
            throw "Cannot attach single multiple match.";
        }
    };    
};

// ADDER implementation
extract.adder = function(def) {
    return extract.adder[def] || extract.adder.append;
};
extract.adder.append = function(input, mapping, ret) {
    var builder = jQuery.listBuilder();
    jQuery.mapEx(input, function() {
        mapping.call(this, builder.adder());
    });
    builder(ret);
};
extract.adder.concat = function(input, mapping, ret) {
    var builder = jQuery.listBuilder();
    jQuery.mapEx(input, function() {
        mapping.call(this, builder.adder());
    });
    builder(function(value) {
        var result = [];
        value.map(function(item) {
            for (var k in item) {
                result.push.apply(result, item[k]);
            }
        });
        ret(result);
    });
};
extract.adder.merge = function(input, mapping, ret) {
    var builder = jQuery.mapBuilder();
    jQuery.mapEx(input, function() {
        mapping.call(this, builder.setter(jQuery.mergeMapList));
    });
    builder(ret);
};

// REGEXP implementation
extract.regexp = function(def) {
    return function() {
        var result = new RegExp(def).exec(this);
        return result ? result[1] : null;
    };
};

// render "JSON" to HTML 
jQuery.render = function(target, obj) {
    var document = target.ownerDocument;
    if (typeof(obj) != "object") {
        if (obj !== null && typeof(obj) !== "undefined") {
            var text = obj.toString();
            if (/^http(s)?\:\/\//.test(text)) {
                var link = document.createElement("a");
                link.setAttribute("href", text);
                link.textContent = text;
                target.appendChild(link);
            } else {
                target.textContent = text;
            }
        }
    } else if (obj instanceof Array) {
        var ul = document.createElement("ul");
        for (var i = 0; i < obj.length; i++) {
            var li = document.createElement("li");
            jQuery.render(li, obj[i]);
            ul.appendChild(li);
        }
        target.appendChild(ul);
    } else {
        var dl = document.createElement("dl");
        for (var k in obj) {
            var dt = document.createElement("dt");
            dt.textContent = k;
            dl.appendChild(dt);
            var dd = document.createElement("dd");
            jQuery.render(dd, obj[k]);
            dl.appendChild(dd);
        }
        target.appendChild(dl);
    }
}
/*
// global window.load handler
jetpack.tabs.onReady(function() {
    if (!this.contentWindow)
        return;
    var window = this.contentWindow.wrappedJSObject;
    var def = window.extbrain_extract_definition;
    if (!def)
        return;
    var document = window.document;
    var body = document.getElementsByTagName("body")[0];
    var log = document.createElement("pre");
    log.setAttribute("class", "log");
    body.appendChild(log);
    extract(def, function(message) {
        log.textContent += message;
    }, function(ret) {
        console.log(ret);
        var output = document.createElement("div");
        output.setAttribute("class", "output");
        jQuery.render(output, ret);
        body.appendChild(output);
        var json = document.createElement("textarea");
        json.setAttribute("class", "json");
        try {
            json.value = JSON.stringify(ret);
        } catch(e) {
            json.value = e;
        }
        body.appendChild(json);
    });
});
*/